1. 简介

Java NIO(New IO Non Blocking IO)是从Java 1.4版本引入的一个新的IO API,其究极目标就是“Speed”,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

2. NIO与IO的区别

IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
选择器(Selectors)

3. Channel

基本上所有的IO和NIO都是基于Channel延伸的。数据可以从Channel通道中读取到Buffer缓冲区,同时也可以从缓冲区中写入到Channel通道中

其中,ByteBuffer是唯一一个能直接与Channel信道通信的 Buffer缓冲区。

  • Java为Channel接口提供的最主要的实现类如下:
    • FileChannel:用于读取、写入、映射和操作文件的通道。
    • DatagramChannel:通过UDP读写网络中的数据通道。
    • SocketChannel:通过TCP读写网络中的数据。
    • ServerSocketChannel:可以监听新进来的TCP连接,像 Web 服务器那样,对每一个新进来的连接都会创建一个SocketChannel。

正如所见,这些通道涵盖了UDP和TCP网络IO,以及文件IO。其中,NIO通过更新IO从而产生了三个新的类,即FileChannel中的FileInputStream,FileOutputSteamRandomAccessFile。Java中的Reader和Writer等字符模式类并没有提供通道的实现或方法。相反的,java.nio.channels.Channels类中提供了实用的获取ReadersWriters方法。

  • FileChannel常用方法
方法 描述
open(Path path, OpenOption… options) 打开或创建一个文件,并返回一个接入该文件的Channel通道
read(ByteBuffer dst) 从Channel中读取数据到ByteBuffer缓冲区中
write(ByteBuffer src) 将ByteBuffer缓冲区中的数据写入到Channel通道中
long position() 返回此通道的文件位置
FileChannel position(long newPosition) 根据文件位置获取文件通道对象
long size() 获取当前文件通道的大小
FileChannel truncate(long size) 将当前通道的文件截取为指定大小
force(boolean metaData) 强制将所有此通道内的文件更见写入到存储设备中
long transferTo(long position, long count, WritableByteChannel target) 将此通道内的指定位置和大小形成的区域写入到目标通道内
long transferFrom(ReadableByteChannel src, long position, long count) 将可读通道内的指定区域写入至当前通道内
  • 基本的Channel示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public void ChannelTest() {
// test.txt内容为1~10
try (RandomAccessFile file = new RandomAccessFile("/test.txt", "rw")) {
// 获取文件通道对象
FileChannel channel = file.getChannel();
// 字节缓冲区(每次读取2个字节)
ByteBuffer buffer = ByteBuffer.allocate(2);
// 获取字节读取结果(返回int为所读取的位置)
int bytesRead = channel.read(buffer);
// 当读取结束则返回-1值
while (bytesRead != -1) {
System.out.println("Read: " + buffer);
// 转换Buffer模式,之前为写入Buffer模式,通过flip()方法转换为读取Buffer模式
buffer.flip();
// 判断是否还有后续字节读取
while (buffer.hasRemaining()) {
System.out.print((char)buffer.get());
}
// 清空缓冲区
buffer.clear();
// 获取字节读取结果(返回int为所读取的位置)
bytesRead = channel.read(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 运行结果
/**
* Read: java.nio.HeapByteBuffer[pos=2 lim=2 cap=2]
* 1
* 2
* Read: java.nio.HeapByteBuffer[pos=2 lim=2 cap=2]
* 3
* 4
* Read: java.nio.HeapByteBuffer[pos=2 lim=2 cap=2]
* 5
* 6
* Read: java.nio.HeapByteBuffer[pos=2 lim=2 cap=2]
* 7
* 8
* Read: java.nio.HeapByteBuffer[pos=2 lim=2 cap=2]
* 9
* 1
* Read: java.nio.HeapByteBuffer[pos=1 lim=2 cap=2]
* 0
*/

注意 buf.flip()的调用,首先读取数据到 Buffer,然后反转 Buffer, 接着再从 Buffer 中读取数据

4. Buffer

缓冲区本质上是一块可以写入区域,然后可以从中读取数据的内存,这块内存被包装为NIO Buffer对象,并提供了一组方法,用来方便地访问该块内存。

4.1 基本用法

使用Buffer读写数据,一般遵循以下四个步骤:

  • 写入数据到Buffer缓冲区中;
  • 调用flip()方法转换读写模式;
  • 从Buffer缓冲区中读取数据;
  • 调用clear()方法或者compact()方法清空Buffer缓冲区。

当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear()compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

4.2 capacity,position 和 limit

  • Buffer中通常包含以下三个属性:
    • Capacity —— 容量
    • Position —— 所在位置
    • Limit —— 最远到达位置

其中position和limit的含义取决于Buffer处于读模式还是写模式。而不管Buffer处于什么模式,capacity的含义都是一样的,表示缓冲区的容量大小。

作为一个内存块,Buffer有一个固定的大小值,称为“capacity”。只能往里边写capacity个byte、long或char等类型,一旦Buffer满了之后,需要将其清空(通过读数据或清除数据),才能继续往Buffer中写数据。

  • position

写数据至Buffer中时,position表示当前的位置。初始的position位置为0,当一个byte、long或char类型等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元,position最大可为capacity - 1,此处对应数组下标。

当从Buffer中读数据时,也是从某个特定位置开始读取,当将Buffer从写模式切换到读模式,position就会被置为0,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

  • limit

在写模式下,Buffer的limit表示最多能往Buffer里写多少数据。写模式下,limit就等于Buffer的capacity。

当切换至读模式时,limit则表示最多能读多少数据。因此**当切换Buffer至读模式时,limit就会被设置成写模式下的position值*(超过写模式下的position值都是空的)***。

4.3 Buffer的类型

Java NIO中有以下八种Buffer类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

由上述几种类型可知,不同的Buffer类型代表了不同的能读写的数据类型,除了MappedByteBuffer外,其他类型的Buffer相对应地可以通过char,short,long,int,byte,float,double等类型来操作缓冲区中的字节。

4.4 Buffer大小分配

必须通过分配大小来实现获取Buffer缓冲区,在allocate方法初始化Buffer的时候,必须为其提供一个capacity参数,其单位通常为字节,具体实例如下所示:

1
2
3
4
# 分配一个48字节的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
# 分配一个可存储1024个字符的CharBuffer
CharBuffer buf = CharBuffer.allocate(1024);

4.5 写数据

写数据到Buffer有两种方式:

  • 从Channel通道中读取数据至Buffer中;
  • 从一个Buffer中使用put()方法写入到另一个Buffer中。

4.6 读数据

从Buffer中读数据有两种方式:

  • 从Buffer中读取数据至Channel通道;
  • 使用get()方法读取Buffer中的数据。

4.7 其他方法

rewind() 方法

rewind() 将 position 设回 0(数据仍然存在),所以可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。

clear() 与 compact() 方法

一旦读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。可以通过clear()compact() 方法来完成。

如果调用的是clear()方法,position 将被设回 0,limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据(源码如下所示)

1
2
3
4
5
6
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}

如果 Buffer 中有一些未读的数据,调用 clear() 方法,数据将 “被遗忘”,意味着不再有任何标记会告诉哪些数据被读过,哪些还没有。

如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用 compact() 方法。

compact() 方法将所有未读的数据拷贝到 Buffer 起始处,也就是整理碎片。然后将 position 设到最后一个未读元素正后面。limit 属性依然像 clear() 方法一样,设置成 capacity。现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。

mark() 与 reset() 方法

通过调用 mark() 方法,可以标记 Buffer 中的一个特定 position。之后可以通过调用 reset() 方法恢复到这个 position。例如:

1
2
3
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.

equals() 与 compareTo() 方法

可以使用 equals()compareTo() 方法两个 Buffer。

equals()

当满足下列条件时,表示两个 Buffer 相等:

  1. 有相同的类型(byte、char、int 等)。
  2. Buffer 中剩余的 byte、char 等的个数相等。
  3. Buffer 中所有剩余的 byte、char 等都相同。

equals 只是比较 Buffer 的一部分,不是每一个在它里面的元素都比较。实际上,它只比较 Buffer 中的剩余元素。

compareTo() 方法

compareTo()方法比较两个 Buffer 的剩余元素 (byte、char 等), 如果满足下列条件,则认为一个 Buffer“小于” 另一个 Buffer:

  1. 第一个不相等的元素小于另一个 Buffer 中对应的元素 。
  2. 所有元素都相等,但第一个 Buffer 比另一个先耗尽 (第一个 Buffer 的元素个数比另一个少)。

Tips:剩余元素是从 position 到 limit 之间的元素。

5. Scatter / Gather

Java NIO开始支持Scatter / GatherScatter / Gather用于描述从Channel通道中读取和写入到Channel的操作。

分散(Scatter)从Channel中读取是指在读操作时将读取的数据写入多个Buffer中。因此Channel将从Channel中读取的数据“分散(Scatter)”到多个Buffer中。

聚集(Gather)是指从多个Buffer中将数据写入至Channel通道中,Channel将多个Buffer中的数据“聚集(Gather)”后发送到Channel。

Scatter / Gather 经常用于需要传输的数据分开处理的场合,例如传输一个由消息头到消息体组成的消息,你可能会将消息体和消息头分散到不同的Buffer中,这样可以方便地处理消息头和消息体。

6. Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样的话,一个单独的线程就能管理多个Channel通道,从而管理多个网络连接。

overview-selectors

6.1 Selector的创建

通过调用Selector.open()方法创建一个Selector,如下所示:

1
Selector selector = Selector.open();

6.2 注册Channel通道

为了使Channel与Selector配合工作,需要在Selector选择器中注册Channel通道,通过Channel.register(Selector sel,int ops)方法实现***(由于Channel必须是非阻塞的,所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SeletableChannel,Socket Channel可以正常使用)***SelectableChannel抽象类有一个configureBlocking()用于使通道处于阻塞模式或非阻塞模式,如下所示:

1
2
3
4
// 开启非阻塞模式
socketChannel.configureBlocking(false);
// 将socketChannel注册入selector中
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_ACCEPT);

其中Channel.register(Selector sel,int ops)方法中的第一个参数指定了注册的目标选择器,而第二个参数指定的是选择器需要查询的通道操作。

可以供选择器查询的通道操作,从类型来分,包括以下四种:

(1)可读 : SelectionKey.OP_READ

(2)可写 : SelectionKey.OP_WRITE

(3)连接 : SelectionKey.OP_CONNECT

(4)接收 : SelectionKey.OP_ACCEPT

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

6.3 Selection Key

在每个Channel通道向Selector进行注册后,都将获得一个Selection Key,类似通常使用的Token,该Selection Key将在Channel通道调用的close()方法或主动调用cancel()方法取消后才失效,而再取消该Selection Key时,并不会立即生效,而是将其添加进Selector中的 cancelled-key Set集合中,待下次selection operation操作时再清除。

在上述注册Channel通道时,register()方法都会返回一个SelectionKey对象,这个对象包含了一些属性:

  • interest集合
  • ready集合
  • Channel
  • Selector
  • Others

interest集合

interest集合是所选择的感兴趣的事件集合,可以通过Selection Key读写interest集合,如下所示:

1
2
3
4
5
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

用”位与”操作interest集合和给定的Selection Key变量,可以确定某个事件是否在interest集合中。

ready集合

ready集合是通道以及准备就绪的操作集合。在一次Selection之后,会首先访问这个ready集合,可以通过以下方式读取ready集合。

1
int readyOps = selectionKey.readyOps();

可以用类似判断interest集合的方法,检测Channel中有什么事件已经就绪,也可以使用以下方法进行判断。

1
2
3
4
5
6
7
8
9
10
// 是否可接受
selectionKey.isAcceptable();
// 是否可连接
selectionKey.isConnectable();
// 是否可读
selectionKey.isReadable();
// 是否合法
selectionKey.isValid();
// 是否可写
selectionKey.isWritable();

获取Channel / Selector

可以通过Selection Key获取Channel通道或Selector选择器,如下所示:

1
2
3
4
// 获取对应的通道
selectionKey.channel();
// 获取对应的选择器
selectionKey.selector();

还可以在用 register() 方法向 Selector 注册 Channel 的时候附加对象。如:

1
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

示例

这里有一个完整的示例,打开一个 Selector,注册一个通道注册到这个 Selector 上 (通道的初始化过程略去), 然后持续监控这个 Selector 的四种事件(接受,连接,读,写)是否就绪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}